package com.hueydink.rhythmrouletteserver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.*;
import java.util.concurrent.*;
import static java.util.concurrent.TimeUnit.*;

public class Server implements Runnable {

	MainWindow window;
	ArrayBlockingQueue<String> abq = new ArrayBlockingQueue<>(10);
	ArrayBlockingQueue<String> connCheckAbq = new ArrayBlockingQueue<>(10);
	CopyOnWriteArrayList<Connection> connections = new CopyOnWriteArrayList<>();
	private ServerSocket serverSocket = null;
	ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
	StringBuilder messageLogText = new StringBuilder("");
	String outMessage;
	String phraseMsg = "";
	Thread retreiverThread = new Thread(new MessageRetreiver(), "retreiver");
	Thread acceptConnectionsThread = new Thread(new AcceptConnections(),
			"accept_cons");
	private static int portNumber;
	int firstPitcherId;
	int numConnections = 0;
	int secondPitcherId;
	int thirdPitcherId;
	int currentPitcherId;
	int resultCount = 0;
	int[] results = new int[3];
	int[] scores = { -1, -1, -1 };
	boolean gameInProgress = false;
	boolean isMessage = false;
	boolean timeExpired = false;
	boolean openPlayerSpot = true;
	boolean openSpotInLine = true;

	// Server constructor
	public Server(MainWindow window) {
		this.window = window;
		portNumber = MainWindow.portNumber;
		// invoke inner class run method to process in-bound message queue
		retreiverThread.start();
		// invoke inner class run method to process new connections
		acceptConnectionsThread.start();
		// schedule periodic checks to find and remove inactive connections
		ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
				1);
		Thread connectionCheckerThread = new Thread(new ConnectionChecker(),
				"conn_checker");
		executor.scheduleAtFixedRate(connectionCheckerThread, 40, 40, SECONDS);
	}

	// run game loop for three rounds
	public void run() {
		int round = 1;
		gameInProgress = true;
		setPlayerOrder();
		int[] pitchers = { firstPitcherId, secondPitcherId, thirdPitcherId };

		for (int i = 0; i < 3; i++) {
			results[i] = -1;
		}

		while ((round <= 3) && (gameInProgress)) {
			manageRound(round, pitchers[round - 1]);
			if (round == 3) {
				sendToAll(MessageUtil.makeUpdateMsg(-1, -1, results[0],
						results[1], results[2]));
			}
			round++;
		}
		gameInProgress = false;
	}

	// send identical messages to each client
	public void sendToAll(String message) {
		for (Connection conn : connections) {
			if (conn != null) {
				conn.sendText(message);
			}
		}
	}

	// append messages to the GUI window
	public void postMessage(String message) {
		if (messageLogText.toString().equals("")) {
			messageLogText.append(message);
			window.display.asyncExec(new Runnable() {
				public void run() {
					if (!window.styledText.isDisposed()) {
						window.styledText.setText(messageLogText.toString());
					}
				}
			});
		} else {
			messageLogText.append("\n" + message);
			window.display.asyncExec(new Runnable() {
				public void run() {
					if (!window.styledText.isDisposed()) {
						window.styledText.setText(messageLogText.toString());
					}
				}
			});
		}
	}

	/**
	 * START OF INBOUND MESSAGE PROCESSING METHODS******************************
	 */
	public void dispatchReceivedMessages(String message) {
		postMessage(message);

		if (message.contains("connectioncheck")) {
			handleConnectionCheckMsg(message);
		}
		if ((message.contains("playrequest")) && (!gameInProgress)) {
			handlePlayRequestMsg(message);
		}
		if (message.contains("phrase")) {
			handlePhraseMsg(message);
		}
		if (message.contains("result")) {
			handleResultMsg(message);
		}
		if ((message.contains("quitrequest")) && (!gameInProgress)) {
			handleQuitRequestMsg(message);
		}
	}

	public void handlePlayRequestMsg(String playRequestMsg) {
		int playerId;

		playerId = Integer.parseInt(String.valueOf(playRequestMsg.charAt(21)));

		if (numConnections < 3) {
			if (playerId > 0) {
				connections.get(playerId - 1).sendText(
						MessageUtil.makeErrorMsg(3));
			}
		} else {
			Thread gameThread = new Thread(this, "game");
			gameThread.start();
		}
	}

	public void handleConnectionCheckMsg(String connCheckMsg) {
		String[] strings1;
		String[] strings2;
		String playerIdString;

		strings1 = connCheckMsg.split("<sender>");
		strings2 = strings1[1].split("</sender>");
		playerIdString = strings2[0];
		connCheckAbq.add(playerIdString);
	}

	public void handlePhraseMsg(String phraseMessage) {
		phraseMsg = phraseMessage;
	}

	public void handleResultMsg(String resultMessage) {
		String[] strings1;
		String[] strings2;
		String scoreString;
		int playerId;
		int score;

		playerId = Integer.parseInt(String.valueOf(resultMessage.charAt(16)));

		strings1 = resultMessage.split("<score>");
		strings2 = strings1[1].split("</score>");
		scoreString = strings2[0];
		score = Integer.parseInt(scoreString);
		scores[playerId - 1] = score;
		resultCount++;
	}

	public void handleQuitRequestMsg(String message) {
		int playerId;
		playerId = Integer.parseInt(String.valueOf(message.charAt(21)));
		Connection connRemoved;

		if (!gameInProgress) {
			connRemoved = connections.set(playerId - 1, null);
			try {
				connRemoved.socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			--numConnections;
			sendToAll(MessageUtil.makeErrorMsg(2));
		}
	}

	/**
	 * END OF INBOUND MESSAGE PROCESSING METHODS********************************
	 */

	/**
	 * START OF GAME MANAGEMENT METHODS*****************************************
	 */
	public void manageRound(int round, int pitcherId) {
		ScheduledFuture<?> future15;
		ScheduledFuture<?> future40;
		ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
				1);
		sendToAll(MessageUtil.makeUpdateMsg(round, pitcherId, results[0],
				results[1], results[2]));

		phraseMsg = "";
		timeExpired = false;

		future15 = executor.schedule(new TimeOut(), 15, SECONDS);
		while ((phraseMsg.equals("")) && (!timeExpired)) {
			try {
				Thread.sleep(250);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		if (timeExpired) {
			// send error messages and cancel game
			sendToAll(MessageUtil.makeErrorMsg(1));
			gameInProgress = false;
			System.out.println("A timeout has occurred.");
		} else {
			future15.cancel(true);
			sendToAll(phraseMsg);
		}

		if (gameInProgress) {
			resultCount = 0;
			timeExpired = false;

			future40 = executor.schedule(new TimeOut(), 40, SECONDS);
			while ((resultCount < 2) && (!timeExpired)) {
				try {
					Thread.sleep(250);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			if (timeExpired) {
				// send error messages and cancel game
				sendToAll(MessageUtil.makeErrorMsg(1));
				gameInProgress = false;
				System.out.println("A timeout has occurred.");
			} else {
				future40.cancel(true);
				computeResults(pitcherId);
			}
		}
	}

	public void setPlayerOrder() {
		int orderCode;
		orderCode = getOrderCode();
		if (orderCode == 1) {
			firstPitcherId = 1;
			secondPitcherId = 2;
			thirdPitcherId = 3;
		} else if (orderCode == 2) {
			firstPitcherId = 1;
			secondPitcherId = 3;
			thirdPitcherId = 2;
		} else if (orderCode == 3) {
			firstPitcherId = 2;
			secondPitcherId = 1;
			thirdPitcherId = 3;
		} else if (orderCode == 4) {
			firstPitcherId = 2;
			secondPitcherId = 3;
			thirdPitcherId = 1;
		} else if (orderCode == 5) {
			firstPitcherId = 3;
			secondPitcherId = 1;
			thirdPitcherId = 2;
		} else {
			firstPitcherId = 3;
			secondPitcherId = 2;
			thirdPitcherId = 1;
		}
	}

	public static int getOrderCode() {
		double randomNum;
		int orderCode;

		randomNum = 6 * Math.random();

		if (randomNum < 1) {
			orderCode = 1;
		} else if (randomNum < 2) {
			orderCode = 2;
		} else if (randomNum < 3) {
			orderCode = 3;
		} else if (randomNum < 4) {
			orderCode = 4;
		} else if (randomNum < 5) {
			orderCode = 5;
		} else {
			orderCode = 6;
		}

		return orderCode;
	}

	public void computeResults(int curPitcherId) {
		if (curPitcherId == 1) {
			results[0] = -1;
			if (scores[1] > scores[2]) {
				results[1] = 1;
				results[2] = 2;
			} else if (scores[2] > scores[1]) {
				results[1] = 2;
				results[2] = 1;
			} else {
				results[1] = 1;
				results[2] = 1;
			}
		}
		if (curPitcherId == 2) {
			results[1] = -1;
			if (scores[0] > scores[2]) {
				results[0] = 1;
				results[2] = 2;
			} else if (scores[2] > scores[1]) {
				results[0] = 2;
				results[2] = 1;
			} else {
				results[0] = 1;
				results[2] = 1;
			}
		}
		if (curPitcherId == 3) {
			results[2] = -1;
			if (scores[0] > scores[1]) {
				results[0] = 1;
				results[1] = 2;
			} else if (scores[1] > scores[0]) {
				results[0] = 2;
				results[1] = 1;
			} else {
				results[0] = 1;
				results[1] = 1;
			}
		}
	}

	/**
	 * END OF GAME MANAGEMENT METHODS*******************************************
	 */

	/**
	 * START OF RUNNABLE INNER CLASSES******************************************
	 */
	private class MessageRetreiver implements Runnable {

		boolean isListening = true;

		public void run() {
			String newMessage = "";

			while (isListening) {
				try {
					newMessage = abq.take();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				dispatchReceivedMessages(newMessage);
			}
		}

	}

	private class AcceptConnections implements Runnable {
		boolean isListening = true;
		int playerSpotNdx;

		public void run() {
			for (int i = 0; i < 3; i++) {
				connections.add(null);
			}

			try {
				serverSocket = new ServerSocket(portNumber);
			} catch (IOException iOException) {
				iOException.printStackTrace();
			}

			while (isListening) {
				playerSpotNdx = connections.indexOf(null);
				if (playerSpotNdx != -1) {
					try {
						connections.set(playerSpotNdx, new Connection(
								serverSocket.accept(), playerSpotNdx));
					} catch (IOException iOException) {
						iOException.printStackTrace();
					}
				}
			}
		}

	}

	private class Connection implements Runnable {

		int playerId;
		private Socket socket;
		PrintWriter printWrtr = null;
		String outMessage;
		boolean isTextToSend;
		Thread connReader;

		// Connection constructor
		public Connection(Socket socket, int playerSpotNdx) {
			this.socket = socket;
			playerId = playerSpotNdx + 1;
			numConnections++;
			outMessage = MessageUtil.makeIdentificationMsg(playerId);
			// wait for client to finish initializing before sending message
			try {
				Thread.sleep(6000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			sendText(outMessage);

			// get input and output streams
			try {
				printWrtr = new PrintWriter(socket.getOutputStream(), true);
			} catch (IOException iOException) {
				iOException.printStackTrace();
			}

			// instantiate and start message reading thread
			connReader = new Thread(this, "conn_reader");
			connReader.start();
			System.out.println("The server has made a connection on port "
					+ this.socket.getPort() + ".");
		}

		public void sendText(String outMessage) {
			this.outMessage = outMessage;
			isTextToSend = true;

			if (isTextToSend) {

				// get and wrap output stream
				try {
					printWrtr = new PrintWriter(socket.getOutputStream(), true);
				} catch (IOException iOException) {
					iOException.printStackTrace();
				}

				// write message to the output stream
				try {
					printWrtr.println(outMessage);
				} catch (Exception e) {
					e.printStackTrace();
				}

				isTextToSend = false;
			}
		}

		public void run() {
			String inMessage;
			boolean isListening = true;

			try {

				// get and wrap input stream
				BufferedReader bufRdr = new BufferedReader(
						new InputStreamReader(socket.getInputStream()));

				while (isListening) {
					if ((inMessage = bufRdr.readLine()) != null) {
						abq.add(inMessage);
					}
				}
			} catch (IOException iOException) {
				System.out.println("A connection has been closed.");
			}
		}

	}

	private class ConnectionChecker implements Runnable {

		public void run() {
			int numOutIds = 0;
			int numInIds = 0;
			int playerId = 0;
			int[] outPlayerIds = new int[10];
			int[] inPlayerIds = new int[10];
			boolean isToBeRemoved = true;
			Connection connRemoved;

			connCheckAbq.clear();
			for (Connection conn : connections) {
				if (conn != null) {
					conn.sendText("<connectioncheck></connectioncheck>");
					playerId = conn.playerId;
					outPlayerIds[numOutIds] = playerId;
					numOutIds++;
				}
			}
			// wait for clients to respond to queries
			try {
				Thread.sleep(6000);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}

			while (!connCheckAbq.isEmpty()) {
				try {
					inPlayerIds[numInIds] = Integer.parseInt(connCheckAbq
							.take());
				} catch (NumberFormatException | InterruptedException e) {
					e.printStackTrace();
				}
				numInIds++;
			}

			while (numInIds > 0) {
				for (int i = 0; i < numOutIds; i++) {
					playerId = outPlayerIds[i];
					if (playerId == inPlayerIds[numInIds - 1]) {
						isToBeRemoved = false;
					}
				}
				if (isToBeRemoved) {
					connRemoved = connections.set(playerId - 1, null);
					try {
						connRemoved.socket.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
					if (!gameInProgress) {
						sendToAll(MessageUtil.makeErrorMsg(2));
					}
					--numConnections;
				}
				numInIds--;
			}
		}

	}

	private class TimeOut implements Runnable {

		public void run() {
			timeExpired = true;
		}

	}
	/**
	 * END OF RUNNABLE INNER CLASSES********************************************
	 */

}